"""
This module contains various classes for interacting with the
Landis-II files stored on disk.
    - DAO: The Data Access Object interface
    - NoDAO: This class is used when there is no model
    - TreeDAO: This class is used when the model is a directory of files
    - FileTree: Used to generate tree structures
    - Model: 
"""
"""
   Copyright 2011 Gabriel Elkind and Brian Gapinski

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
"""

import os
import exceptions
import re

"""
For the time being, the FileTree and Model class have been kept
seperate for several reasons: 

-The FileTree is a more abstract structure which
may be useful in other contexts within lime
-The two serve different but related purposes:
A FileTree object's duty is simply to serve
and modify items blindly, while Model objects serve
to provide an interface for sanely interacting with
the underlyng files.

Of course, the two may be revised to fit into a single class
at a later time.
"""

class FileTree(object):
    """
    This class provides methods for generating a tree
    
    @ivar _name:      The name of the tree
    @type _name:      string

    @ivar _save_flag: Whether or not the buffer in lime is in
                      sync with the file on disk
    @type _save_flag: bool

    @ivar _children:  A list of this trees child trees
    @type _children:  list of FileTrees
    """
    def __init__(self,name):
        """
        Creates a FileTree

        @param name: The name of the new FileTree
        @type name:  string
        """
        #: The filepath at the head of this tree
        self._name = name

        #: Whether or not the file on disk is in sync with
        #: the editor buffer in lime
        self._save_flag = True

        #: The list of subtrees contained by this tree
        self._children = []

    def AddChild(self,child):
        """
        Adds a child to the list of children

        @param child: The child to be added
        @type child: ?
        """
        self._children.append(child)

    def SetSync(self,new_flag_value):
        """
        Recursively searches the tree for the specified file. If found,
        change save_flag of the file.
        
        @param new_flag_value: The new value of the specified file
        @type new_flag_value: bool
        """
        self._save_flag = new_flag_value

    def GetName(self):
        """
        Returns the name of the tree

        @return: The name of the tree
        @rtype: string
        """
        return self._name

    def GetChildren(self):
        """
        Returns the children contained in the tree

        @return: The children of this tree
        @rtype: list of ?
        """
        return self._children

    def GetSync(self):
        """
        Returns the value of the save flag

        @return: The value of the save flag
        @rtype: bool
        """
        return self._save_flag

    def __repr__(self):
        return self._print_tree(0)

    def _print_tree(self,depth):
        string = 4*depth*" " + self._name
        for child in self._children:
            string += '\n'+ child._print_tree(depth+1)
        return string

class DAO(object):
    """
    Objects which extend this class can be used by Model objects to access 
    internal representations of Landis files. It is abstracted from the rest of
    the Model like this so that it may be swapped out depending on the format 
    of the project data the Model object needs to access.

    Note that this is an abstract class meant only for specification, as such
    all methods will throw errors if they have not been implemented by a 
    subclass
    """
    def __init__(self):
        """
        Creates a new Data Access Object. This will
        never be used at all
        """
        raise NotImplementedError

    def GetTree(self):
        """
        This class represents the underlying filestructure
        for a Landis-II project. It can be generated from a 
        directory containing Landis-II project files.
        """
        raise NotImplementedError

    def GetSync(self,name,tree=None):
        """
        Returns the value of the save flag

        @param name: The name of the file to check
        @type name: string

        @return: True if name is in sync, False otherwise
        @rtype: bool
        """ 
        raise NotImplementedError
           
    def GetAllFiles(self):
        """
        Recursively collects and returns a list of every file
        contained within the model.
        
        @return: A list of every file contained in the
                 specified tree
        @rtype: list of trees
        """
        raise NotImplementedError

    def SetSync(self,name,new_flag_value):
        """
        Sets a mark representing whether or not the specified
        file is in sync with the suspended buffer
        
        @param name: The filename to modify
        @type name: string
        
        @param new_flag_value: The new value of the flag
        @type new_flag_value: bool
        """
        raise NotImplementedError
        
    def Save(self, text, filename):
        """
        Saves the information contained in text to the
        file named filename. This completely overwrites
        all data contained in the file.

        @param text: The text to be written
        @type text: string
        
        @param filename: The name of the file to be written
        @type filename: string
        """
        raise NotImplementedError


class NoDAO(DAO):
    """
    A DAO that has access to no data
    """
    def __init__(self):
        """
        Creates a new Data Access Object. This will
        never be used at all
        """
        pass

    def GetTree(self):
        """
        returns the internal FileTree object

        @return: The representation of the tree hierarchy
        @rtype: tree
        """
        return FileTree('')

    def GetSync(self,name):
        """
        Returns the value of the save flag

        @param name: The name of the file to check
        @type name: string

        @return: True if name is in sync, False otherwise
        @rtype: bool
        """ 
        pass
           
    def GetAllFiles(self):
        """
        Recursively collects and returns a list of every file
        contained within the model.
        
        @return: A list of every file contained in the
                 specified tree
        @rtype: list of trees
        """
        return []

    def SetSync(self,name,new_flag_value):
        """
        Sets a mark representing whether or not the specified
        file is in sync with the suspended buffer
        
        @param name: The filename to modify
        @type name: string
        
        @param new_flag_value: The new value of the flag
        @type new_flag_value: bool
        """
        pass
        
    def Save(self, text, filename):
        """
        Saves the information contained in text to the
        file named filename. This completely overwrites
        all data contained in the file.

        @param text: The text to be written
        @type text: string
        
        @param filename: The name of the file to be written
        @type filename: string
        """
        pass

class TreeDAO(DAO):
    """
    A DAO which uses an underlying FileTree to organize
    files in a directory in a heirarchical fashion
    """
    def __init__(self,filename):
        """
        Makes a new TreeDAO
        """
        self._rootfile = filename
        self._filetree = self._GenerateTree(filename) 

# Consider adding later to facilitate improper user file
# selection
#
#    def _FindRoot(self):
#        """
#        Finds the root of the file hierarchy
#        """
#        listing = os.listdir(self._directory)
#        for filename in listing:
#            if filename.endswith('.txt'):
#                filepath = os.path.join(self._directory,filename)
#                if not os.path.isdir(filepath):
#                    ofile = open(filepath, "r")
#                    first_line = ofile.readline()
#                    if 'LandisData  Scenario' in first_line:
#                        return filename
#        raise RuntimeError('No scenario file has been discovered') 


    def _GenerateTree(self,filepath):
        """
        Generates the tree hierarchy

        @param filepath: The path to the file directory
        @type filepath: string

        @return: The tree representation of the file hierarchy
        @rtype: tree
        """
        #Safely parse the file if it exists
        if os.path.exists(filepath):
            ofile = open(filepath, 'r')
        else:
            raise IOError(4,filepath)

        filetree = FileTree(filepath)
            
        for line in ofile:
            
            # Ignores commented lines
            if not line.startswith('>>'):

                #search for references to .txt files
                match = re.search("[.\w-]*txt",line)

                #append them recursively when found
                if match:
                    subtree = self._GenerateTree(match.group(0))
                    filetree.AddChild(subtree)
        ofile.close()        
        return filetree

    def GetTree(self):
        """
        returns the internal FileTree object

        @return: The representation of the tree hierarchy
        @rtype: tree
        """
        return self._filetree

    def GetSync(self,name,tree=None):
        """
        Gets the sync state of a given tree

        @param name: The name of the tree to get the state of
        @type name: string

        @param tree: used for performing recursive depth first search
                     searches from the root file when None is given
        @type tree: FileTree

        @rtype: bool
        @return: True if the flag is in sync, False otherwise
        """
        #If the tree is None, search from root
        if tree == None:
            tree = self._filetree

        #return the sync value if this is the tree
        #we're looking for
        if name == tree._name:
            return tree.GetSync()

        #otherwise, perform recursive depth first search
        else:
            for subtree in tree.GetChildren():
                flag = self.GetSync(name,subtree)
                if not flag == None:
                    return flag
        return None

    def GetAllFiles(self,tree=None,filecollector=[]):
        """
        Recursively collects and returns a list of every file
        contained within the model.
        
        @param tree: The tree to search, if None search 
                     all files in the model
        @type tree: tree
        
        @param filecollector: The list to fill with the files
        @type filecollector: list
        
        @returns: A list of every file contained in the
                  specified tree
        @rtype: list of trees
        """
        if tree == None:
            tree = self._filetree
        filecollector.append(tree.GetName())
        for child in tree.GetChildren():
            self.GetAllFiles(child,filecollector)
        return filecollector

    def SetSync(self,name,new_flag_value,tree=None):
        """
        Sets a mark representing whether or not the specified
        file is in sync with the suspended buffer
        
        @param name: The filename to modify
        @type name: string

        @param new_flag_value: The new value of the flag
        @type new_flag_value: bool

        @param tree: The tree to be searched
        @type tree: tree
        """
        if tree == None:
            tree = self._filetree
        if name == tree._name:
            tree.SetSync(new_flag_value)
        else:
            for child in tree.GetChildren():
                self.SetSync(name,new_flag_value,child)
  
    def Save(self, text, filename):
        """
        Saves the information contained in text to the
        file named filename. This completely overwrites
        all data contained in the file.

        @param text: The text to be written
        @type text: string
        
        @param filename: The name of the file to be written
        @type filename: string
        """
        f = open(filename, 'w')
        f.write(text)
        f.close()
    
class Model(object):
    """
    Used to interact with the DAOs and the subject for an observer
    pattern with the graphical components created by Lime.

    @ivar _dao: The DAO of this model
    @type _dao: DAO

    @ivar _observers: The list of components that are notified when a
                      change occurs in the model
    @type _observers: list
    """
    def __init__(self):
        """
        This class represents the underlying filestructure 
        for a Landis-II project. It can be generated from a
        directory containing Landis-II project files.
        """
        self._dao = NoDAO()
        self._observers = []
    
    def GetTree(self):
        """
        returns the internal FileTree object

        @return: The representation of the tree hierarchy
        @rtype: tree
        """
        return self._dao.GetTree()

    def GetSync(self,name):
        """
        Check whether or not the specified file is 
        in sync with the current editor

        @param name: The name of the file to check
        @type name:  string

        @return: True if name is in sync, False otherwise
        @rtype: bool
        """
        return self._dao.GetSync(name)
        
    def GetAllFiles(self):
        """
        Recursively collects and returns a list of every file
        contained within the model.
        
        @returns: A list of every file contained in the
                  specified tree
        @rtype: list of trees
        """
        return self._dao.GetAllFiles()
        
    def SetSync(self,name,new_flag_value):
        """
        Sets a mark representing whether or not the specified
        file is in sync with the suspended buffer

        @param name: The filename to modify
        @type name: string
        
        @param new_flag_value: The new value of the flag
        @type new_flag_value: bool
        """
        self._dao.SetSync(name,new_flag_value)
        self.NotifyAll(name)
   
    def SetProject(self,rootfile):
        """
        This method is used to change the files that model represents.
        
        @param rootfile: The first file to parse, from which all other
                         project files will be identified
        @type rootfile:  string
        """
        #So in this part I'm thinking we have some static dictionary that
        #pairs regular expressions with DAO types and have this method
        #search through the specified file and pick which kind of DAO to make
        self._dao = TreeDAO(rootfile)
        self.NotifyAll()

    def Register(self, observer):
        """
        Notes an observer to be notified whenever
        files within this model undergo modifications
        
        @param observer: The component to be notified
        @type observer:  Any object with a .Notify(filename) fn
        """
        self._observers.append(observer)
        observer.SetModel(self)

    def NotifyAll(self,filename=''):
        """
        Notify all relevant components that a file has
        changed within this model. 
        
        @param filename: The file which has changed
                         or None if the project
                         has been changed
        @type filename:  string
        """
        for observer in self._observers:
            observer.Notify(filename)
    
    def Save(self, text, filename):
        """
        Saves the information contained in text to the
        file named filename. This completely overwrites
        all data contained in the file.

        @param text: The text to be written
        @type text: string
        
        @param filename: The name of the file to be written
        @type filename: string
        """
        self._dao.Save(text, filename)
        self.SetSync(filename, True)
